<?php

/*
Aurus CMS - free OOP CMS
Copyright (C) 2010  Nestor Yanchuk

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/
if (!defined('ROOT'))
die();

/**
 * Клас для обробки BBcodes
 * 
 * @package Aurus
 * @author Kvis
 * @copyright 2010
 * @version 1.0
 * @access public
 */
class Parser implements Core {

	/**
     * Return an instance of class
     *
     * @return Parser
     */
	public static function getInstance() {
		return Factory::get(__CLASS__);
	}

	public $allow_html = false;
	public $max_img_size = 500;
	public $allow_index_url = false;
	public $wordwrap = 0;
	public $censore = array();
	public $censoreWord = '*@#$%_*';
	/**
     * @ignore
     */
	private $_text = '';
	/**
     * @ignore
     */
	private $_tree = array();
	/**
     * @ignore
     */
	private $_tagBlacklist = array('applet', 'body', 'bgsound', 'base', 'basefont', 'embed', 'frame', 'frameset', 'head', 'html', 'id', 'iframe', 'ilayer', 'layer', 'link', 'meta', 'name', 'object', 'script', 'style', 'title', 'xml');
	/**
     * @ignore
     */
	private $_attrBlacklist = array('action', 'background', 'codebase', 'dynsrc', 'lowsrc', 'onblur', 'onclick', 'ondblclick', 'onfocus', 'onkeydown', 'onkeypress', 'onkeyup', 'onmousedown', 'onmouseout', 'onmouseover', 'onmouseup');
	/**
     * @ignore
     */
	private $font_sizes = array(1 => '8', 2 => '10', 3 => '12', 4 => '14', 5 => '18', 6 => '24', 7 => '36');
	/**
     * @ignore
     */
	private $_tagList = array();
	/**
     * @ignore
     */
	private $_tagHandlers = array();
	/**
     * @ignore
     */
	private $unParse_rules = array();
	public $allowBr = true;

	/**
     * Конструктор класу
     * 
     * @return void
     */
	function __construct() {
		$dh = @opendir(dirname(__file__) . '/parser_tags/');
		if (!$dh)
		die('No directory: "' . dirname(__file__) . '/parser_tags/"');
		while (!is_bool($ddt = readdir($dh))) {
			if ($ddt == '.' or $ddt == '..' or is_dir(dirname(__file__) . '/parser_tags/' . $ddt))
			continue;
			$this->_tagList[] = substr($ddt, 0, -4);
		}
		closedir($dh);

		$this->allow_html = Config::get('allow_html', 'class_parser');
		$this->max_img_size = Config::get('max_img_size', 'class_parser');
		$this->allow_index_url = Config::get('allow_index_url', 'class_parser');
		$this->wordwrap = Config::get('wordwrap', 'class_parser');
		$this->censore = array_map('trim', explode(',', Config::get('censoreWords', 'class_parser')));
		$this->censoreWord = Config::get('censoreWordsReplasement', 'class_parser');
	}

	/**
     * @ignore
     */
	private function prepareText() {
		if (!$this->allow_html) {
			$array['<'] = '&#60;';
			$array['>'] = '&#62;';
		}
		$array['"'] = '&#34;';
		$array['\''] = '&#39;';
		$array['&'] = '&#38;';
		$array['`'] = '&#96;';
		$array['  '] = '&nbsp;&nbsp;';
		$array["\r"] = '';
		$this->_text = strtr($this->_text, $array);
		$this->_text = preg_replace("#\<iframe#i", '#60;iframe', $this->_text);
		$this->_text = preg_replace("#\<script#i", '#60;script', $this->_text);
		if ($this->allow_html) {
			$this->_text = $this->prepareHtml();
		}

		if (trim(Config::get('censoreWords', 'class_parser'))) {
			foreach ($this->censore as $badWord) {
				$this->_text = preg_replace('~' . preg_quote(strtolower($badWord)) . '~i', $this->censoreWord, $this->_text);
			}
		}
	}

	/**
     * @ignore
     */
	private function wordwrap($text, $intWordLenght) {
		$texA = explode(' ', $text);
		$ret = array();

		foreach ($texA as $word) {
			if (mb_strlen($word) > $intWordLenght) {
				$ret[] = mb_substr($word, 0, $intWordLenght);
				$ret[] = mb_substr($word, $intWordLenght);
			} else {
				$ret[] = $word;
			}
		}

		return implode(' ', $ret);
	}

	/**
     * @ignore
     */
	private function prepareHtml() {
		foreach ($this->_tagBlacklist as $tag) {
			if (is_int(strpos($this->_text, $tag))) {
				$this->_text = preg_replace("~<{$tag}~i", "&#60;{$tag}", $this->_text);
			}
		}
		foreach ($this->_attrBlacklist as $attr) {
			if (is_int(strpos($this->_text, $attr))) {
				$this->_text = preg_replace("~{$attr}=~i", "{$attr}&#61;", $this->_text);
			}
		}
	}

	/**
     * Розбір строки
     *
     * @param string $text - початкова строка
     * @return string - оброблена строка
     */
	public function parse($text) {
		$aurus = Aurus::getInstance();

		if (function_exists('get_magic_quotes_gpc') && get_magic_quotes_gpc())
		$text = stripslashes($text);
		$this->_text = $text;
		$this->prepareText();
		$this->forEachText();

		$smls = Aurus::getInstance()->getSmilies();
		for ($i = 0, $n = count($smls); $i < $n; $i++) {
			$this->_text = str_replace('::' . $smls[$i] . '::', '<!--smile:' . $smls[$i] . '--><img border="0" src="' . LINK_ROOT . '/smiles/' . $smls[$i] . '" alt="::' . $smls[$i] . '::" /><!--/smile-->', $this->_text);
		}

		return $this->_text;
	}

	/**
     * @ignore
     */
	private function getAutomatState($symvol) {
		if ($symvol == '[')
		return 1;
		if ($symvol == ']')
		return 2;
		if ($symvol == '=')
		return 3;
		if ($symvol == '/')
		return 4;
		if ((ord($symvol) < 123) && (ord($symvol) > 64))
		return 5;
		return 0;
	}

	/**
     * @ignore
     */
	private function forEachText() {
		/*
		* 0 - Every symvol
		* 1 - [
		* 2 - ]
		* 3 - =
		* 4 - /
		* 5 - A-Za-z
		*/
		$prevState = 0;
		$FSM = array(//p
		//r
		//e
		//v
		//s
		//t
		//a
		//t
		//e
		//_________0__1__2__3__4__5_ -  current automat state
		0 => array(0, 1, 0, 0, 0, 0),
		1 => array(0, 1, 0, 0, 4, 5),
		2 => array(0, 1, 0, 0, 0, 0),
		3 => array(3, 3, 2, 3, 3, 3),
		4 => array(0, 1, 0, 0, 0, 5),
		5 => array(0, 1, 2, 3, 3, 5));

		$this->_tree = array();

		$cont = '';
		$prevState = 0;
		$par = '';
		$tag = '';
		$subLevel = 0;

		$nodes[] = &$this->_tree;

		for ($i = 0, $n = strlen($this->_text); $i < $n; $i++) {
			$s = $this->_text[$i];
			$state = $this->getAutomatState($s);
			switch ($FSM[$prevState][$state]) {
				//Open tag
				case 1:
					//		if ((int)$this->wordwrap >= 50) {
					//			$cont = $this->wordwrap($cont, (int)$this->wordwrap);
					//		}
					$nodes[$subLevel][] = array('type' => 'text', 'text' => $cont);
					$cont = $s;
					break;
					//close tag
				case 2:
					$cont .= $s;
					$tag = explode('=', ($cont[1] == '/') ? substr($cont, 2, -1) : substr($cont, 1, -1));

					if (in_array($tag[0], $this->_tagList)) { //Check if tag exists
						if ($cont[1] == '/') { //close tag
							$subLevel--;
						} else {
							$nodes[$subLevel + 1] = &$nodes[$subLevel][];
							$nodes[++$subLevel][] = array('type' => 'tag', 'tag' => $tag[0], 'param' => isset($tag[1]) ? $tag[1] : '');
						}
						$cont = '';
					} else {
						if ((int) $this->wordwrap >= 50) {
							$cont = $this->wordwrap($cont, (int) $this->wordwrap);
						}
						$nodes[$subLevel][] = array('type' => 'text', 'text' => $cont);
						$cont = '';
					}

					break;

					//add to tag
				case 0:
				case 3:
				case 4:
				case 5:
					$cont .= $s;
					break;
			}
			$prevState = $FSM[$prevState][$state];
		}

		$nodes[$subLevel][] = array('type' => 'text', 'text' => $cont);
		$cont = '';

		$this->_text = $this->processTree($this->_tree);
	}

	/**
     * @ignore
     */
	private function processTree($tree) {
		$ret = '';
		$tag_opened = false;
		$tagInner = '';
		$tag = '';
		$tagParam = '';
		foreach ($tree as $num => $elem) {
			if (!isset($elem['type'])) {
				if ($tag_opened) {
					if ($this->_tagHandlers[$tag]->tags_inside) {
						$tagInner .= $this->processTree($elem);
					} else {
						$tagInner .= $this->getAllTagsWithNoParsing($elem);
					}
				} else {
					$ret .= $this->processTree($elem);
				}
			} elseif ($elem['type'] == 'text') {
				if ($tag_opened) {
					$tagInner .= nl2br($elem['text']);
				} else {
					$ret .= nl2br($elem['text']);
				}
			} elseif ($elem['type'] == 'tag') {
				$tag = $elem['tag'];
				$tagParam = $elem['param'];
				$tag_opened = true;
				$this->tagClassInit($tag);
			}
		}

		if ($tag_opened) {
			if (trim($tagInner) == '') {
				$tagParam = $tagParam ? '=' . $tagParam : '';
				$ret .= '[' . $tag . $tagParam . ']' . '[/' . $tag . ']';
			} else {
				$ret .= $this->tagProcess($tag, $tagInner, $tagParam);
			}
		}
		return $ret;
	}

	/**
     * @ignore
     */
	private function tagClassInit($tag) {
		if (!isset($this->_tagHandlers[$tag]) && $tag) {
			include (dirname(__file__) . '/parser_tags/' . $tag . '.php');
			$class = 'Parser_Tag_' . $tag;
			$this->_tagHandlers[$tag] = new $class;
		}
	}

	/**
     * @ignore
     */
	private function tagProcess($tag, $inner, $param = '') {
		$text = $this->_tagHandlers[$tag]->get($inner, $param);
		if ($this->_tagHandlers[$tag]->allowBr) {
			$text = nl2br($text);
		} 
		return $text;
	}

	/**
     * @ignore
     */
	private function getAllTagsWithNoParsing($tree) {
		$ret = '';
		$tag_opened = false;
		$tagInner = '';
		$tag = '';
		$tagParam = '';

		foreach ($tree as $num => $elem) {
			if (!isset($elem['type'])) {
				$ret .= $this->getAllTagsWithNoParsing($elem);
			} else
			if ($elem['type'] == 'text') {
				$ret .= $elem['text'];
			} else
			if ($elem['type'] == 'tag') {
				$tag_opened = true;
				$tag = $elem['tag'];
				$p = $elem['param'] ? '=' . $elem['param'] : '';
				$ret .= '[' . $elem['tag'] . $p . ']';
			}
		}
		if ($tag_opened) {
			$ret .= '[/' . $tag . ']';
		}
		return $ret;
	}

	/**
     * Перетворення HTML коду в BBcodes
     *
     * @param string $text - HTML код
     * @return string - оброблена строка
     */
	public function unParseBB($text) {
		if (function_exists('get_magic_quotes_gpc') && get_magic_quotes_gpc())
		$text = stripslashes($text);
		$text = str_replace('<br />', RN, $text);
		$text = preg_replace('~\<!--smile:(.+?)--\>.+?\<\!--\/smile--\>~is', '::$1::', $text);

		foreach ($this->_tagList as $tag) {
			$this->tagClassInit($tag);
			$text = $this->_tagHandlers[$tag]->unparse($text);
		}

		return $text;
	}

}